package org.dmikis.jmmengine.models;

import static java.lang.Math.abs;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.HashMap;

import javax.swing.JPanel;

/**
 * @author dmikis
 * @version 0.4
 * 
 * Implementation of two-dimensional scalar discrete field
 * with fixed size.
 */
public class Scalar2DField implements Result {
    
    /**
     * Create new field with <b>x</b> by <b>y</b> points
     * 
     * @param width width of the field
     * @param height height of the field
     * @throws IllegalArgumentException if one or both parameters are negative
     */
    public Scalar2DField(int width, int height) throws IllegalArgumentException {
	if (width <= 0 || height <= 0) {
	    throw new IllegalArgumentException("Sizes of field must be positive numbers!");
	}
	this.size = new Dimension(width, height);
	this.initializeKernel();
	this.initializeVisualization();
    }
    
    /**
     * Create new field with given by Dimension object size
     * 
     * @param size size of new field
     * @throws IllegalArgumentException if width or height field of Dimension
     * 					object is negative
     */
    public Scalar2DField(Dimension size) throws IllegalArgumentException {
	if (size.height <= 0 || size.width <= 0) {
	    throw new IllegalArgumentException("Sizes of field must be positive numbers!");
	}
	this.size = (Dimension) size.clone();
	this.initializeKernel();
	this.initializeVisualization();
    }
    
    /**
     * Create kernel of the field as 2D-array and initialize it by zero values
     */
    private void initializeKernel() {
	this.kernel = new HashMap<Point2D, Double>();
	for (int x = 0; x < this.size.width; x++) {
	    for (int y = 0; y < this.size.height; y++) {
		this.kernel.put(new Point2D(x, y), new Double(0.0));
	    }
	}
    }
    
    /**
     * Create empty visual image
     */
    private void initializeVisualization() {
	this.visualization = new BufferedImage(this.size.width, this.size.height, BufferedImage.TYPE_INT_RGB);
    }

    /**
     * Get field's value at point (x,y)
     * 
     * @param x x-coordinate of the requested point
     * @param y y-coordinate of the requested point
     * @return value of field at the requested point
     * @throws IndexOutOfBoundsException if the requested point lies out of the field's boundaries
     */
    public double getPointValue(int x, int y) throws IndexOutOfBoundsException {
	// TODO: add negative number checks and null value handling
	if (x >= this.size.width || y >= this.size.height) {
	    throw new IndexOutOfBoundsException();
	}
	return this.kernel.get(new Point2D(x, y)).doubleValue();
    }
    
    /**
     * Set new value to field's point
     * 
     * @param x x-coordinate of the point
     * @param y y-coordinate of the point
     * @param value new value of the point
     * @throws IndexOutOfBoundsException if the requested point lies out of the field's boundaries
     */
    public void setPointValue(int x, int y, double value) throws IndexOutOfBoundsException {
	// TODO: add negative number checks
	if (x >= this.size.width || y >= this.size.height) {
	    throw new IndexOutOfBoundsException();
	}
	this.maxValue = (value > this.maxValue) ? value : this.maxValue;
	this.minValue = (value < this.minValue) ? value : this.minValue;
	this.kernel.put(new Point2D(x, y), value);
    }

    /**
     * Get size of field
     * 
     * @return Dimension object with field's size 
     */
    public Dimension getSize() {
	return (Dimension) this.size.clone();
    }
    
    /**
     * Render new visual image of field.
     */
    public void renderFieldVisualization() {
	BufferedImage newVisualization = new BufferedImage(this.size.width, this.size.height, BufferedImage.TYPE_INT_RGB);
	for (int x = 0; x < this.size.width; x++) {
	    for (int y = 0; y < this.size.height; y++) {
		double fieldValue = this.kernel.get(new Point2D(x, y));
		float normalizedValue = abs((float) ((fieldValue - this.minValue) / (this.maxValue - this.minValue)));
		try {
		    Color color = new Color(normalizedValue, normalizedValue, normalizedValue);
		    newVisualization.setRGB(x, y, color.getRGB());
		}
		catch (IllegalArgumentException e) {
		    System.err.printf("Render point (%d;%d) error, color: %f\n", x, y, normalizedValue);
		    newVisualization.setRGB(x, y, Color.RED.getRGB());
		}
	    }
	}
	this.visualization = newVisualization;
	this.visualizationPanel.repaint();
    }
    
    /**
     * Get panel contains field image scaled up to panel's size.
     * 
     * @see org.dmikis.jmmengine.Result#getVisualizationPanel
     */
    @Override
    public JPanel getVisualizationPanel() {
	return this.visualizationPanel;
    }

    protected Dimension size;
    
    protected double maxValue = Double.MIN_VALUE;
    protected double minValue = Double.MAX_VALUE;

    //private double[][] kernel;
    protected HashMap<Point2D, Double> kernel;
    protected BufferedImage visualization;
    protected volatile PlotPanel visualizationPanel = new PlotPanel();
    
    protected class Point2D {
	
	public Point2D(int x, int y) {
	    this.x = x;
	    this.y = y;
	}
	
	@Override
	public boolean equals(Object other) {
	    if (other instanceof Point2D) {
		Point2D otherPoint2D = (Point2D) other;
		return this.x == otherPoint2D.x && this.y == otherPoint2D.y;
	    }
	    return false;
	}
	
	@Override
	public int hashCode() {
	    String t = Integer.toString(x) + Integer.toString(y);
	    return t.hashCode();
	}
	
	protected int x;
	protected int y;
    }
    
    @SuppressWarnings("serial")
    protected class PlotPanel extends JPanel {
	
	public void paintComponent(Graphics g) {
	    // TODO: Implement correct resizing of scalar field up to frame dimensions;
	    g.clearRect(0, 0, getSize().width-1, getSize().height-1);
	    Graphics2D g2 = (Graphics2D) g;
	    double scaleX = (double) (this.getWidth()) / size.getWidth(); // size - field from Scalar2D class
	    double scaleY = (double) (this.getHeight()) / size.getHeight();
	    g2.drawImage(visualization, AffineTransform.getScaleInstance(scaleX, scaleY), null);
	}
    }
}
